// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: hyuan

#include "coin2/exchange/kucoin_futures/order_v1/parser.h"

#include <sstream>

#include "coin2/exchange/kucoin/api_order/native_private_client.h"
#include "coin2/exchange/kucoin/order_v1/error_code.h"
#include "coin2/exchange/kucoin_futures/api_order/native_private_client.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::kucoin_futures::order_v1 {

namespace {

using namespace coin2::exchange::kucoin::order_v1::error_code;

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::CurrencyTransferElement;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;
using coin2::exchange::base::order::IOrderContext;
using coin2::exchange::kucoin_futures::symbology::KucoinFuturesCurrency;
using coin2::exchange::kucoin_futures::symbology::KucoinFuturesProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;

inline int64_t GetProcOrderId(const JsonValue& data) {
  if (data.HasMember("clientOid") && data["clientOid"].IsString()) {
    if (!data["clientOid"].IsNull()) {
      return stoll(data["clientOid"].GetString());
    }
  }
  return 0;
}

inline coin::proto::OrderSide GetOrderSide(const std::string& side) {
  if (side == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline coin::proto::OrderType GetOrderType(const std::string& type) {
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

inline void GetOrderStatus(ProductOrderElement* order, bool is_active, bool cancel_exist) {
  if (is_active) {
    order->set_is_live(true);
  } else {
    order->set_is_live(false);
    if (cancel_exist) {
      order->set_cancel_confirmed(true);
    } else {
      order->set_fully_filled(true);
    }
  }
}

inline void SetOrderStatus(ProductOrderElement* order, const std::string& type) {
  if (type == "open") {
    order->set_is_live(true);
  } else if (type == "match") {
    order->set_is_live(true);
  } else if (type == "canceled") {
    order->set_is_live(false);
    order->set_cancel_confirmed(true);
  } else if (type == "filled") {
    order->set_is_live(false);
    order->set_fully_filled(true);
  }
}

inline int64_t GetTimestamp(const std::string& time_str) { return stoll(time_str) * 1e6; }

inline bool IsKucoinFuturesErrMessage(const JsonValue& doc) {
  if (doc.HasMember("code")) {
    return (std::string(doc["code"].GetString()) != "200000");
  }
  return false;
}

inline bool IsOrderUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  if (json.HasMember("topic") && json.HasMember("data") && json["data"].HasMember("type")) {
    if (json["topic"] == "/contractMarket/tradeOrders") {
      return (json["data"]["type"] != "match");
    }
  }
  return false;
}

inline bool IsTradeUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  if (json.HasMember("topic") && json.HasMember("data") && json["data"].HasMember("type")) {
    if (json["topic"] == "/contractMarket/tradeOrders") {
      return (json["data"]["type"] == "match");
    }
  }
  return false;
}

inline bool IsBalanceUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  if (json.HasMember("topic") && json.HasMember("subject") && json.HasMember("data")) {
    return (json["topic"] == "/contractAccount/wallet") &&
           (json["subject"] == "availableBalance.change");
  }
  return false;
}

inline bool IsPositionUpdate(const rapidjson::GenericValue<rapidjson::UTF8<>>& json) {
  if (json.HasMember("topic") && json.HasMember("subject") && json.HasMember("data")) {
    std::string topic = json["topic"].GetString();
    return (
        boost::starts_with(topic, "/contract/position") && (json["subject"] == "position.change"));
  }
  return false;
}

}  // namespace

void KucoinFuturesParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsKucoinFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Error message.\n" << JsonToString(document);
    return;
  }

  auto account_balance = ParseBalance(document["data"]);
  if (!is_account_balance_initialized_) {
    result_.set_type(MsgType::IGNORABLE);
    return;
  }
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

void KucoinFuturesParser::SaveBalanceToCacheMap(const JsonValue& doc) {
  auto* currency = symcache_->GetCurrencyByNative(doc["currency"].GetString());
  std::string native_currency = currency->native_currency();
  if (balance_cache_map_.count(native_currency) == 0) {
    balance_cache_map_.emplace(native_currency, CurrencyBalance{});
  }
  auto& balance = balance_cache_map_.at(native_currency);
  balance.set_currency(currency->currency());
  balance.set_currency_native(native_currency);
  double total = stod(doc["accountEquity"].GetString());
  double avail = stod(doc["availableBalance"].GetString());
  balance.set_total(total);
  balance.set_available(avail);
  balance.set_hold(total - avail);
}

void KucoinFuturesParser::GetBalanceFromCacheMap(AccountBalance& acc_balance) {
  for (auto& data : balance_cache_map_) {
    auto* currency_balance = acc_balance.add_each_balance();
    currency_balance->MergeFrom(data.second);
    auto* currency = symcache_->GetCurrencyByNative(data.first);
    currency_balance->set_currency(currency->currency());
    currency_balance->set_currency_native(currency->native_currency());
  }
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
auto KucoinFuturesParser::ParseBalance(const JsonValue& doc) -> AccountBalance {
  AccountBalance acc_balance;

  if (!doc.HasMember("currency") || !doc.HasMember("accountEquity") ||
      !doc.HasMember("availableBalance")) {
    LOG(ERROR) << "[ParseBalance] Invalid data format.\n" << JsonToString(doc);
    return acc_balance;
  }

  if (!is_account_balance_initialized_) {
    if (symcache_->has_currency_native(doc["currency"].GetString())) {
      SaveBalanceToCacheMap(doc);
    }
    if (balance_cache_map_.size() == GetNativeCurrencies().size()) {
      acc_balance.set_exchange("Kucoin");
      acc_balance.set_market_type("Futures");
      GetBalanceFromCacheMap(acc_balance);
      is_account_balance_initialized_ = true;
    }
    return acc_balance;
  }

  acc_balance.set_exchange("Kucoin");
  acc_balance.set_market_type("Futures");
  if (symcache_->has_currency_native(doc["currency"].GetString())) {
    auto* currency = symcache_->GetCurrencyByNative(doc["currency"].GetString());
    auto* currency_balance = acc_balance.add_each_balance();
    currency_balance->set_currency(currency->currency());
    currency_balance->set_currency_native(currency->native_currency());
    double total = stod(doc["accountEquity"].GetString());
    double avail = stod(doc["availableBalance"].GetString());
    currency_balance->set_total(total);
    currency_balance->set_available(avail);
    currency_balance->set_hold(total - avail);
  }

  return acc_balance;
}

void KucoinFuturesParser::ParseRestAccountPosition() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountPosition] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsKucoinFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseRestAccountPosition] Error message.\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("data") || !document["data"].IsArray()) {
    LOG(ERROR) << "[ParseRestAccountPosition] Invalid data format.\n" << JsonToString(document);
    return;
  }

  auto account_position = ParsePosition(document);
  if (!account_position.has_exchange() || !account_position.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_POSITION);
  *result_.mutable_account_position() = std::move(account_position);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountPosition KucoinFuturesParser::ParsePosition(const JsonValue& doc) {
  RawPositionMap raw_position_map;
  auto success = ParseRawPosition(doc, &raw_position_map);
  if (!success) {
    return {};
  }

  AccountPosition acc_position;
  acc_position.set_exchange("Kucoin");
  acc_position.set_market_type("Futures");
  for (const auto& native_symbol : symcache_->GetProductNative()) {
    if (raw_position_map.count(native_symbol) != 0) {
      continue;
    }
    auto* position = acc_position.add_each_position();
    position->set_long_position(0);
    position->set_short_position(0);
    position->set_net_position(0);
    position->set_available_long_position(0);
    position->set_available_short_position(0);
    position->set_native_symbol(native_symbol);

    DLOG(INFO) << "POS: " << native_symbol << " " << position->net_position();
  }

  for (auto& pair : raw_position_map) {
    auto* position = acc_position.add_each_position();
    *position = std::move(pair.second);
    position->set_native_symbol(pair.first);
    DLOG(INFO) << "POS: " << pair.first << " " << position->net_position();
  }
  is_account_position_initialized_ = true;

  return acc_position;
}

bool KucoinFuturesParser::ParseRawPosition(
    const JsonValue& document,
    RawPositionMap* raw_position_map) {
  /*
  {
    'code': '200000',
    'data': {
      'id': '5e81a7827911f40008e80715',
      'symbol': 'XBTUSDTM',
      'autoDeposit': False,
      'maintMarginReq': 0.005,
      'riskLimit': 2000000,
      'realLeverage': 5.0,
      'crossMode': False,
      'delevPercentage': 0.35,
      'openingTimestamp': 1623832410892,
      'currentTimestamp': 1623832488929,
      'currentQty': 1,
      'currentCost': 40.008,
      'currentComm': 0.0240048,
      'unrealisedCost': 40.008,
      'realisedGrossCost': 0.0,
      'realisedCost': 0.0240048,
      'isOpen': True,
      'markPrice': 40014.93,
      'markValue': 40.01493,
      'posCost': 40.008,
      'posCross': 0.0,
      'posInit': 8.0016,
      'posComm': 0.02880576,
      'posLoss': 0.0,
      'posMargin': 8.03040576,
      'posMaint': 0.23284656,
      'maintMargin': 8.03733576,
      'realisedGrossPnl': 0.0,
      'realisedPnl': -0.0240048,
      'unrealisedPnl': 0.00693,
      'unrealisedPnlPcnt': 0.0002,
      'unrealisedRoePcnt': 0.0009,
      'avgEntryPrice': 40008.0,
      'liquidationPrice': 32211.0,
      'bankruptPrice': 32006.0,
      'settleCurrency': 'USDT'
    }
  }
  */
  for (const auto& data : document["data"].GetArray()) {
    if (!data.HasMember("symbol") || !data.HasMember("currentQty")) {
      LOG(ERROR) << "[ParseRawPosition] Invalid position data\n" << JsonToString(document);
      continue;
    }
    auto native_symbol = data["symbol"].GetString();
    auto& position = (*raw_position_map)[native_symbol];
    double pos_amt = stod(data["currentQty"].GetString());
    if (pos_amt >= 0.) {
      position.set_long_position(pos_amt);
    } else {
      position.set_short_position(-pos_amt);
    }
    position.set_net_position(pos_amt);
  }

  return true;
}

void KucoinFuturesParser::ParseRestOpenOrderInfoList() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsKucoinFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] Error message.\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("data") || !document["data"].HasMember("items") ||
      !document["data"]["items"].IsArray()) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] Invalid data format.\n" << JsonToString(document);
    return;
  }

  ProductOrderBundle* product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;
  for (const auto& data : document["data"]["items"].GetArray()) {
    if (!data.HasMember("symbol")) {
      continue;
    }
    std::string native_symbol = data["symbol"].GetString();
    if (!symcache_->has_product_native(native_symbol)) {
      continue;
    }
    auto order = product_order_bundle->add_each_order();
    order->Clear();
    ParseRestOrderInfo(data, order);
    product_order_bundle_found = true;
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void KucoinFuturesParser::ParseRestOrderInfo(const JsonValue& data, ProductOrderElement* order) {
  /*
  {
    "code": "200000",
    "data": {
      "currentPage": 1,
      "pageSize": 100,
      "totalNum": 1000,
      "totalPage": 10,
      "items": [
        {
          "id": "5cdfc138b21023a909e5ad55",
          "symbol": "XBTUSDM",
          "type": "limit",
          "side": "buy",
          "price": "3600",
          "size": 20000,
          "value": "56.1167227833",
          "filledValue": "0",
          "filledSize": 0,
          "stp": "",
          "stop": "",
          "stopPriceType": "",
          "stopTriggered": false,
          "stopPrice": null,
          "timeInForce": "GTC",
          "postOnly": false,
          "hidden": false,
          "iceberg": false,
          "visibleSize": null,
          "leverage": "20",
          "forceHold": false,
          "closeOrder": false,
          "reduceOnly": false,
          "clientOid": "5ce24c16b210233c36ee321d",
          "remark": null,
          "isActive": true,
          "cancelExist": false,
          "createdAt": 1558167872000,
          "settleCurrency": "XBT",
          "status": "open",
          "updatedAt": 1558167872000,
          "orderTime": 1558167872000000000
        }
      ]
    }
  }
  */
  order->set_exchange_type(ExchangeType::Kucoin);
  order->set_market_type(MarketType::Futures);
  order->set_api_version("v1");
  order->set_exchange_order_id(data["id"].GetString());
  order->set_symbol(data["symbol"].GetString());
  order->set_price(stod(data["price"].GetString()));
  order->set_qty(stod(data["size"].GetString()));
  order->set_proc_order_id(GetProcOrderId(data));
  order->set_side(GetOrderSide(data["side"].GetString()));
  order->set_order_type(GetOrderType(data["type"].GetString()));
  order->set_timestamp(GetTimestamp(data["createdAt"].GetString()));
  GetOrderStatus(order, data["isActive"].GetBool(), data["cancelExist"].GetBool());
}

void KucoinFuturesParser::ParseRestHistOrderInfoList() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestHistOrderInfoList] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsKucoinFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseRestHistOrderInfoList] Error message.\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("data") || !document["data"].HasMember("items") ||
      !document["data"]["items"].IsArray()) {
    LOG(ERROR) << "[ParseRestHistOrderInfoList] Invalid data format.\n" << JsonToString(document);
    return;
  }

  ProductOrderBundle* product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;
  for (const auto& data : document["data"]["items"].GetArray()) {
    if (!data.HasMember("symbol")) {
      continue;
    }
    std::string native_symbol = data["symbol"].GetString();
    if (!symcache_->has_product_native(native_symbol)) {
      continue;
    }
    auto order = product_order_bundle->add_each_order();
    order->Clear();
    ParseRestOrderInfo(data, order);
    product_order_bundle_found = true;
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void KucoinFuturesParser::ParseRestFillInfoList() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestFillInfoList] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsKucoinFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseRestFillInfoList] Error message.\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("data") || !document["data"].HasMember("items") ||
      !document["data"]["items"].IsArray()) {
    LOG(ERROR) << "[ParseRestFillInfoList] Invalid data format.\n" << JsonToString(document);
    return;
  }

  ProductFillBundle* product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  bool product_fill_bundle_found = false;
  for (const auto& data : document["data"]["items"].GetArray()) {
    if (!data.IsObject() || !data.HasMember("tradeType") || data["tradeType"].IsNull() ||
        !data["tradeType"].IsString() || std::string(data["tradeType"].GetString()) != "trade") {
      LOG(ERROR) << "[ParseRestFillInfoList] Invalid data format.\n" << JsonToString(data);
      continue;
    }

    auto fill = product_fill_bundle->add_each_fill();
    fill->Clear();
    ParseRestFillInfo(data, fill);
    product_fill_bundle_found = true;
  }

  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void KucoinFuturesParser::ParseRestFillInfo(const JsonValue& data, ProductFillElement* fill) {
  /*
  {
    "code": "200000",
    "data": {
      "currentPage":1,
      "pageSize":1,
      "totalNum":251915,
      "totalPage":251915,
      "items":[
        {
          "symbol": "XBTUSDM",
          "tradeId": "5ce24c1f0c19fc3c58edc47c",
          "orderId": "5ce24c16b210233c36ee321d",
          "side": "sell",
          "liquidity": "taker",
          "price": "8302",
          "size": 10,
          "value": "0.001204529",
          "feeRate": "0.0005",
          "fixFee": "0.00000006",
          "feeCurrency": "XBT",
          "stop": "",
          "fee": "0.0000012022",
          "orderType": "limit",
          "tradeType": "trade",
          "createdAt": 1558334496000,
          "settleCurrency": "XBT",
          "tradeTime": 1558334496000000000
        }
      ]
    }
  }
  */
  fill->set_symbol(data["symbol"].GetString());
  fill->set_exchange_order_id(data["orderId"].GetString());
  fill->set_price(stod(data["price"].GetString()));
  fill->set_qty(stod(data["size"].GetString()));
  fill->set_side(GetOrderSide(data["side"].GetString()));
  fill->set_fill_id(data["tradeId"].GetString());
  fill->set_fill_timestamp(msg_->timestamp);
  if (data.HasMember("fee") && !data["fee"].IsNull() && data.HasMember("feeCurrency") &&
      !data["feeCurrency"].IsNull()) {
    fill->set_fee(stod(data["fee"].GetString()));
    fill->set_fee_currency(data["feeCurrency"].GetString());
  }
  std::string role = data["liquidity"].GetString();
  if (role == "maker") {
    fill->set_fill_type(FillType::MAKER_FILL_TYPE);
  } else if (role == "taker") {
    fill->set_fill_type(FillType::TAKER_FILL_TYPE);
  } else {
    fill->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
  }
}

void KucoinFuturesParser::ParseRestBullet() {
  /*
  {
    "code": "200000",
    "data": {
      "instanceServers": [
        {
          "pingInterval": 50000,
          "endpoint": "wss://push.kucoin.com/endpoint",
          "protocol": "websocket",
          "encrypt": true,
          "pingTimeout": 10000
        }
      ],
      "token": "..."
    }
  }
  */
  if (is_bullet_initialized_) {
    return;
  }

  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestBullet] Parsing error.\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (IsKucoinFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseRestBullet] Error message.\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("data") || !document["data"].HasMember("token") ||
      !document["data"].HasMember("instanceServers") ||
      !document["data"]["instanceServers"].IsArray() ||
      (document["data"]["instanceServers"].GetArray().Size() <= 0)) {
    LOG(ERROR) << "[ParseRestBullet] Invalid data format.\n" << JsonToString(document);
    return;
  }

  const auto& data = document["data"]["instanceServers"].GetArray()[0];
  if (!data.HasMember("endpoint") || !data.HasMember("encrypt") ||
      !data.HasMember("pingInterval") || !data.HasMember("pingTimeout")) {
    LOG(ERROR) << "[ParseRestBullet] Invalid bullet format.\n" << JsonToString(document);
    return;
  }

  std::vector<std::string> bullet_info;
  bullet_info.push_back(data["endpoint"].GetString());
  bullet_info.push_back(data["encrypt"].GetBool() ? "true" : "false");
  bullet_info.push_back(data["pingInterval"].GetString());
  bullet_info.push_back(data["pingTimeout"].GetString());
  bullet_info.push_back(document["data"]["token"].GetString());
  mb_->PublishEvent("kucoin/bullet", &bullet_info);
  is_bullet_initialized_ = true;
}

void KucoinFuturesParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(GetCurrentTimestamp());

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseSubmitResponse] AsioHttpContextStatus error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[ParseSubmitResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("code") && json.HasMember("msg")) {
    auto error_code = GetOrderErrorCode(json["code"], json["msg"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(json));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[ParseSubmitResponse] Error response\n" << http_context->res;
      if (error_code == coin::proto::OrderErrorCode::ORDER_UNCERTAIN) {
        result_.Clear();
        result_.set_type(MsgType::IGNORABLE);
      } else {
        resp->set_success(false);
      }
      return;
    }
  }

  resp->set_success(true);
  if (json.HasMember("data") && json["data"].HasMember("orderId")) {
    resp->set_exchange_order_id(json["data"]["orderId"].GetString());
  } else {
    LOG(ERROR) << "[ParseSubmitResponse] Invalid data format\n" << JsonToString(json);
  }
}

void KucoinFuturesParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseCancelResponse] AsioHttpContextStatus error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[ParseCancelResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("code") && json.HasMember("msg")) {
    auto error_code = GetOrderErrorCode(json["code"], json["msg"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(json));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[ParseCancelResponse] Error response\n" << http_context->res;
      resp->set_success(false);
      return;
    }
  }

  resp->set_success(true);
  if (json.HasMember("data") && json["data"].HasMember("cancelledOrderIds") &&
      json["data"]["cancelledOrderIds"].IsArray() &&
      (json["data"]["cancelledOrderIds"].GetArray().Size() > 0)) {
    const auto& order_ids = json["data"]["cancelledOrderIds"].GetArray();
    resp->set_exchange_order_id(order_ids[0].GetString());
  } else {
    LOG(ERROR) << "[ParseCancelResponse] Invalid data format\n" << JsonToString(json);
  }
  resp->set_cancel_confirmed(true);
}

int32_t KucoinFuturesParser::ParseErrorMessage(
    const std::shared_ptr<AsioHttpContext>& http_context) {
  enable_time_.success = false;

  const auto http_code = http_context->res.result_int();
  if (http_code == 400) {
    LOG(ERROR) << "[ParseErrorMessage] Bad Request\n" << msg_->DebugString();
  } else if (http_code == 401) {
    LOG(ERROR) << "[ParseErrorMessage] Unauthorized\n" << msg_->DebugString();
  } else if (http_code == 403) {
    LOG(ERROR) << "[ParseErrorMessage] Forbidden\n" << msg_->DebugString();
  } else if (http_code == 404) {
    LOG(ERROR) << "[ParseErrorMessage] Not Found\n" << msg_->DebugString();
  } else if (http_code == 405) {
    LOG(ERROR) << "[ParseErrorMessage] Method Not Allowed\n" << msg_->DebugString();
  } else if (http_code == 415) {
    LOG(ERROR) << "[ParseErrorMessage] Content-Type\n" << msg_->DebugString();
  } else if (http_code == 429) {
    std::string body(http_context->res.body().data());
    if (body.find("1015") != std::string::npos) {
      enable_time_.enable_time = 30 * 1'000'000'000LL + GetCurrentTimestamp();
      enable_time_.success = true;
    } else if (body.find("200002") != std::string::npos) {
      enable_time_.enable_time = 10 * 1'000'000'000LL + GetCurrentTimestamp();
      enable_time_.success = true;
    }
    LOG(ERROR) << "[ParseErrorMessage] Too Many Requests\n" << msg_->DebugString();
  } else if (http_code == 500) {
    LOG(ERROR) << "[ParseErrorMessage] Internal Server Error\n" << msg_->DebugString();
  } else if (http_code == 503) {
    LOG(ERROR) << "[ParseErrorMessage] Service Unavailable\n" << msg_->DebugString();
  }

  return http_code;
}

void KucoinFuturesParser::ParseWsOrderInfoList(const JsonDocument& document) {
  /*
  {
    "type": "message",
    "topic": "/contractMarket/tradeOrders",
    "subject": "orderChange",
    "channelType": "private",
    "data": {
      "orderId": "5cdfc138b21023a909e5ad55",
      "symbol": "XBTUSDM",
      "type": "match",
      "status": "open",
      "matchSize": "",
      "matchPrice": "",
      "orderType": "limit",
      "side": "buy",
      "price": "3600",
      "size": "20000",
      "remainSize": "20001",
      "filledSize":"20000",
      "canceledSize": "0",
      "tradeId": "5ce24c16b210233c36eexxxx",
      "clientOid": "5ce24c16b210233c36ee321d",
      "orderTime": 1545914149935808589,
      "oldSize ": "15000",
      "liquidity": "maker",
      "ts": 1545914149935808589
    }
  }
  */
  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  auto order = product_order_bundle->add_each_order();
  const auto& data = document["data"];
  order->set_exchange_type(ExchangeType::Kucoin);
  order->set_market_type(MarketType::Futures);
  order->set_api_version("v1");
  order->set_exchange_order_id(data["orderId"].GetString());
  order->set_symbol(data["symbol"].GetString());
  order->set_price(stod(data["price"].GetString()));
  order->set_qty(stod(data["size"].GetString()));
  order->set_proc_order_id(GetProcOrderId(data));
  order->set_side(GetOrderSide(data["side"].GetString()));
  order->set_order_type(
      GetOrderType(data.HasMember("orderType") ? data["orderType"].GetString() : ""));
  order->set_timestamp(stoll(data["orderTime"].GetString()));
  SetOrderStatus(order, data["type"].GetString());

  result_.set_type(MsgType::ORDER_STATUS);
}

void KucoinFuturesParser::ParseWsTradeInfoList(const JsonDocument& document) {
  /*
  {
    "type": "message",
    "topic": "/contractMarket/tradeOrders",
    "subject": "orderChange",
    "channelType": "private",
    "data": {
      "orderId": "5cdfc138b21023a909e5ad55",
      "symbol": "XBTUSDM",
      "type": "match",
      "status": "open",
      "matchSize": "",
      "matchPrice": "",
      "orderType": "limit",
      "side": "buy",
      "price": "3600",
      "size": "20000",
      "remainSize": "20001",
      "filledSize":"20000",
      "canceledSize": "0",
      "tradeId": "5ce24c16b210233c36eexxxx",
      "clientOid": "5ce24c16b210233c36ee321d",
      "orderTime": 1545914149935808589,
      "oldSize ": "15000",
      "liquidity": "maker",
      "ts": 1545914149935808589
    }
  }
  */
  ProductFillBundle* product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  auto fill = product_fill_bundle->add_each_fill();
  const auto& data = document["data"];
  fill->set_symbol(data["symbol"].GetString());
  fill->set_exchange_order_id(data["orderId"].GetString());
  fill->set_price(stod(data["matchPrice"].GetString()));
  fill->set_qty(stod(data["matchSize"].GetString()));
  fill->set_side(GetOrderSide(data["side"].GetString()));
  fill->set_fill_id(data["tradeId"].GetString());
  fill->set_fill_timestamp(msg_->timestamp);
  std::string role = data["liquidity"].GetString();
  if (role == "maker") {
    fill->set_fill_type(FillType::MAKER_FILL_TYPE);
  } else if (role == "taker") {
    fill->set_fill_type(FillType::TAKER_FILL_TYPE);
  } else {
    fill->set_fill_type(FillType::UNKNOWN_FILL_TYPE);
  }

  result_.set_type(MsgType::ORDER_FILL);
}

void KucoinFuturesParser::ParseWsBalanceInfoList(const JsonDocument& document) {
  /*
  {
    "userId": "xbc453tg732eba53a88ggyt8c",
    "topic": "/contractAccount/wallet",
    "subject": "availableBalance.change",
    "data": {
      "availableBalance": 5923,
      "holdBalance": 2312,
      "currency":"USDT",
      "timestamp": 1553842862614
    }
  }
  */
  if (!is_account_balance_initialized_) {
    return;
  }

  if (IsKucoinFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseWsBalanceInfoList] Error message.\n" << JsonToString(document);
    return;
  }

  auto acc_balance = result_.mutable_account_balance();
  acc_balance->Clear();
  acc_balance->set_exchange("Kucoin");
  acc_balance->set_market_type("Futures");
  const auto& data = document["data"];
  if (!data.HasMember("currency") || !data.HasMember("holdBalance") ||
      !data.HasMember("availableBalance")) {
    return;
  }
  if (!symcache_->has_currency_native(data["currency"].GetString())) {
    return;
  }
  auto* currency = symcache_->GetCurrencyByNative(data["currency"].GetString());
  auto* currency_balance = acc_balance->add_each_balance();
  currency_balance->set_currency(currency->currency());
  currency_balance->set_currency_native(currency->native_currency());
  auto avail = stod(data["availableBalance"].GetString());
  auto hold = stod(data["holdBalance"].GetString());
  currency_balance->set_total(avail + hold);
  currency_balance->set_available(avail);
  currency_balance->set_hold(hold);

  result_.set_type(MsgType::ACCOUNT_BALANCE);
}

void KucoinFuturesParser::ParseWsPositionInfoList(const JsonDocument& document) {
  /*
  {
    "userId": "5c32d69203aa676ce4b543c7",
    "topic": "/contract/position:XBTUSDM",
    "subject": "position.change",
      "data": {
        "realisedGrossPnl": 0E-8,
        "crossMode": false,
        "liquidationPrice": 1000000.0,
        "posLoss": 0E-8,
        "avgEntryPrice": 7508.22,
        "unrealisedPnl": -0.00014735,
        "markPrice": 7947.83,
        "posMargin": 0.00266779,
        "riskLimit": 200,
        "unrealisedCost": 0.00266375,
        "posComm": 0.00000392,
        "posMaint": 0.00001724,
        "posCost": 0.00266375,
        "maintMarginReq": 0.005,
        "bankruptPrice": 1000000.0,
        "realisedCost": 0.00000271,
        "markValue": 0.00251640,
        "posInit": 0.00266375,
        "realisedPnl": -0.00000253,
        "maintMargin": 0.00252044,
        "realLeverage": 1.06,
        "currentCost": 0.00266375,
        "openingTimestamp": 1558433191000,
        "currentQty": -20,
        "delevPercentage": 0.52,
        "currentComm": 0.00000271,
        "realisedGrossCost": 0E-8,
        "isOpen": true,
        "posCross": 1.2E-7,
        "currentTimestamp": 1558506060394,
        "unrealisedRoePcnt": -0.0553,
        "unrealisedPnlPcnt": -0.0553,
        "settleCurrency": "XBT"
      }
  }
  */
  if (!is_account_position_initialized_) {
    return;
  }

  if (IsKucoinFuturesErrMessage(document)) {
    LOG(ERROR) << "[ParseWsPositionInfoList] Error message.\n" << JsonToString(document);
    return;
  }

  if (!document["data"].HasMember("changeReason") ||
      (document["data"]["changeReason"] != "positionChange") ||
      !document["data"].HasMember("symbol") || !document["data"].HasMember("currentQty")) {
    return;
  }

  auto acc_position = result_.mutable_account_position();
  acc_position->Clear();
  acc_position->set_exchange("Kucoin");
  acc_position->set_market_type("Futures");
  auto* position = acc_position->add_each_position();
  position->set_native_symbol(document["data"]["symbol"].GetString());
  double pos_amt = stod(document["data"]["currentQty"].GetString());
  if (pos_amt >= 0.) {
    position->set_long_position(pos_amt);
  } else {
    position->set_short_position(-pos_amt);
  }
  position->set_net_position(pos_amt);

  result_.set_type(MsgType::ACCOUNT_POSITION);
}

void KucoinFuturesParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  if (IsOrderUpdate(document)) {
    ParseWsOrderInfoList(document);
  } else if (IsTradeUpdate(document)) {
    ParseWsTradeInfoList(document);
  } else if (IsBalanceUpdate(document)) {
    ParseWsBalanceInfoList(document);
  } else if (IsPositionUpdate(document)) {
    ParseWsPositionInfoList(document);
  }
}

auto KucoinFuturesParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  CHECK(msg.type == OrderSubTopicId::REST_MESSAGE);

  int32_t error_code = ParseErrorMessage(msg.http_context);
  if (error_code == 429) {
    if (enable_time_.success) {
      mb_->PublishEvent("kucoin/enable_time", reinterpret_cast<void*>(enable_time_.enable_time));
    }
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::MULTI_CANCEL_RESPONSE:
      // pass
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
      ParseRestOpenOrderInfoList();
      break;
    case OrderSubTopicId::REST_DONE_ORDERS:
      ParseRestHistOrderInfoList();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillInfoList();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    case OrderSubTopicId::REST_ACCOUNT_POSITION:
      ParseRestAccountPosition();
      break;
    // case KucoinOrderSubTopicId::REST_BULLET:
    case OrderSubTopicId::LISTEN_KEY:
      ParseRestBullet();
      break;
    default: {
      LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::kucoin_futures::order_v1
